package com.nulldev.util.io.FastIO;

import static com.nulldev.util.io.FastIO.ExecutionMode.getEM;

import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.NoSuchElementException;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutionPreference;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.executors.bqde.BQDE;
import com.nulldev.util.concurrency.threadIt.v4.executors.fje.ForkJoinExecutorService;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.manage.Version;

/**
 * Solution for bottlenecked IO, via a separate I/O thread provided by the
 * threadIt! v4 API. <br>
 * 
 * @author null
 * @since nullUtil 2.7 (early build 1.0), improved in 3.0
 */
public final class FastIO extends Thread implements Closeable {

	public static abstract class IOSubmission {
		public abstract void onCallable(final PrintWriter OUTPUT, final PrintWriter OUTPUT_ERROR);
	}

	/**
	 * TODO: <br>
	 * - Reduce object creation count.c <br>
	 * - Deprecate println(PrintStream, String) <br>
	 * - Deprecate print(PrintStream, String) <br>
	 * Completed: <br>
	 * - Use direct FD-based writers. <br>
	 * - Cleanup. <br>
	 * - Remove sync on System.err on System.out ops. <br>
	 * - Consider adding an option to use global ExecutorManager. <br>
	 * - Check that using the global ExecutorManager is a good idea. <br>
	 */
	private volatile boolean stop;
	private final LinkedTransferQueue<IOSubmission> submissions = new LinkedTransferQueue<IOSubmission>();
	public static final Version VERSION = new Version("1.0.2");

	private static final FastIO instance = new FastIO();
	private static boolean init;
	static {
		if (!init) {
			Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {

				@Override
				public void run() {
					IOUtils.closeQuietly(instance);
				}
			}, "nullUtil 3.0: FastIO Shutdown Hook"));
			init = true;
		}
	}
	// OUTPUT <- System.out
	private final PrintWriter OUTPUT;
	// OUTPUT_ERROR <- System.err
	private final PrintWriter OUTPUT_ERROR;

	private final static boolean DEBUG_ALWAYS_FLUSH = Arguments.hasArgument("--FastIO.alwaysFlush");
	private final static boolean DISABLE_OUTPUT_BUFFERING = Arguments.notArguments("--FastIO.enableOutputBuffering");
	private final static boolean USE_FJE_FOR_DTP = Arguments.notArguments("--FastIO.useBQDE");

	private FastIO() {
		super("FastIO: Thread");
		if (DISABLE_OUTPUT_BUFFERING) {
			this.OUTPUT = new PrintWriter(new OutputStreamWriter(new FileOutputStream(java.io.FileDescriptor.out)));
			this.OUTPUT_ERROR = new PrintWriter(new OutputStreamWriter(new FileOutputStream(java.io.FileDescriptor.err)));
		} else {
			this.OUTPUT = new PrintWriter(
					new BufferedWriter(new OutputStreamWriter(new FileOutputStream(java.io.FileDescriptor.out)), IOUtils.MEMORY_ALLOC_BUFFER));
			this.OUTPUT_ERROR = new PrintWriter(
					new BufferedWriter(new OutputStreamWriter(new FileOutputStream(java.io.FileDescriptor.err)), IOUtils.MEMORY_ALLOC_BUFFER));
		}
		switch (getEM()) {
			case DEDICATED_THREAD: {
				super.start();
				break;
			}
			case GLOBAL_EXECUTOR: {
				ExecutorManager.get().queueTask(this, ExecutionPreference.DEDICATED_THREAD);
				break;
			}
			case DEDICATED_THREAD_POOL:
			default: {
				if (USE_FJE_FOR_DTP) {
					new ForkJoinExecutorService("FastIO-pool-", 4).submit(this);
				} else {
					new BQDE(4, 2).submit(this);
				}
				break;
			}
		}
	}

	@Override
	public void close() throws IOException {
		this.stop = true;
		IOUtils.closeQuietly(OUTPUT);
		IOUtils.closeQuietly(OUTPUT_ERROR);
		this.submissions.clear();
	}

	@Override
	public void start() {
	}

	private static final int FLUSH_THRESHOLD_MILLIS = 380;

	@Override
	public void run() {
		while (true) {
			try {
				if (FastIO.this.stop) {
					break;
				}
				final IOSubmission iosub = FastIO.this.submissions.poll(30, TimeUnit.SECONDS);
				if (iosub == null) {
					continue;
				}
				final long s = System.currentTimeMillis();
				iosub.onCallable(this.OUTPUT, this.OUTPUT_ERROR);
				final long e = System.currentTimeMillis();
				if ((e - s) > FLUSH_THRESHOLD_MILLIS || this.submissions.isEmpty() || DEBUG_ALWAYS_FLUSH) {
					this.OUTPUT.flush();
					this.OUTPUT_ERROR.flush();
				}
			} catch (Throwable e) {
				if (e != null) {
					if (e instanceof NoSuchElementException || e instanceof InterruptedException) {
					} else {
						e.printStackTrace(this.OUTPUT_ERROR);
					}
				}
			}
		}
	}

	public boolean submit(final IOSubmission submission) {
		try {
			if (!this.submissions.tryTransfer(submission, 233L, TimeUnit.MILLISECONDS)) {
				this.submissions.put(submission);
				return true;
			} else {
				return true;
			}
		} catch (InterruptedException _ignored) {
			return false;
		}
	}

	public void println(final String text) {
		this.submit(new IOSubmission() {

			@Override
			public void onCallable(final PrintWriter OUTPUT, final PrintWriter OUTPUT_ERROR) {
				synchronized (OUTPUT) {
					OUTPUT.print(text);
					OUTPUT.println();
					OUTPUT.flush();
				}
			}
		});
	}

	public void println(final Object text) {
		this.submit(new IOSubmission() {

			@Override
			public void onCallable(final PrintWriter OUTPUT, final PrintWriter OUTPUT_ERROR) {
				synchronized (OUTPUT) {
					OUTPUT.print(text);
					OUTPUT.println();
					OUTPUT.flush();
				}
			}
		});
	}

	public void printlnError(final Throwable err) {
		this.submit(new IOSubmission() {

			@Override
			public void onCallable(final PrintWriter OUTPUT, final PrintWriter OUTPUT_ERROR) {
				synchronized (OUTPUT_ERROR) {
					err.printStackTrace(OUTPUT_ERROR);
					OUTPUT_ERROR.flush();
				}
			}
		});
	}

	public void printlnError(final Object text) {
		this.submit(new IOSubmission() {

			@Override
			public void onCallable(final PrintWriter OUTPUT, final PrintWriter OUTPUT_ERROR) {
				synchronized (OUTPUT_ERROR) {
					OUTPUT_ERROR.print(text);
					OUTPUT_ERROR.println();
					OUTPUT_ERROR.flush();
				}
			}
		});
	}

	public void print(final String text) {
		this.submit(new IOSubmission() {

			@Override
			public void onCallable(final PrintWriter OUTPUT, final PrintWriter OUTPUT_ERROR) {
				synchronized (OUTPUT) {
					OUTPUT.print(text);
					OUTPUT.flush();
				}
			}
		});
	}

	public void print(final Object text) {
		this.submit(new IOSubmission() {

			@Override
			public void onCallable(final PrintWriter OUTPUT, final PrintWriter OUTPUT_ERROR) {
				synchronized (OUTPUT) {
					OUTPUT.print(text);
					OUTPUT.flush();
				}
			}
		});
	}

	public void printError(final Object text) {
		this.submit(new IOSubmission() {

			@Override
			public void onCallable(final PrintWriter OUTPUT, final PrintWriter OUTPUT_ERROR) {
				synchronized (OUTPUT_ERROR) {
					OUTPUT_ERROR.print(text);
					OUTPUT_ERROR.flush();
				}
			}
		});
	}

	public void println(final PrintStream outputDevice, final String text) {
		this.submit(new IOSubmission() {

			@Override
			public void onCallable(final PrintWriter OUTPUT, final PrintWriter OUTPUT_ERROR) {
				synchronized (outputDevice) {
					outputDevice.print(text);
					outputDevice.println();
					outputDevice.flush();
				}
			}
		});
	}

	public void println(final PrintStream outputDevice, final Object text) {
		this.submit(new IOSubmission() {

			@Override
			public void onCallable(final PrintWriter OUTPUT, final PrintWriter OUTPUT_ERROR) {
				synchronized (outputDevice) {
					outputDevice.print(text);
					outputDevice.println();
					outputDevice.flush();
				}
			}
		});
	}

	public void print(final PrintStream outputDevice, final String text) {
		this.submit(new IOSubmission() {

			@Override
			public void onCallable(final PrintWriter OUTPUT, final PrintWriter OUTPUT_ERROR) {
				synchronized (outputDevice) {
					outputDevice.print(text);
					outputDevice.flush();
				}
			}
		});
	}

	public static FastIO get() {
		return instance;
	}

	public void flush() {
		this.OUTPUT.flush();
		this.OUTPUT_ERROR.flush();
		while (!this.submissions.isEmpty()) {
			try {
				Thread.sleep(5L);
			} catch (InterruptedException e) {
				Thread.interrupted();
			}
		}
	}

	public void errln(final Object e) {
		this.submit(new IOSubmission() {

			@Override
			public void onCallable(final PrintWriter OUTPUT, final PrintWriter OUTPUT_ERROR) {
				synchronized (OUTPUT_ERROR) {
					OUTPUT_ERROR.println(e);
					OUTPUT_ERROR.flush();
				}
			}
		});
	}

	public void errln(final Throwable e) {
		this.submit(new IOSubmission() {

			@Override
			public void onCallable(final PrintWriter OUTPUT, final PrintWriter OUTPUT_ERROR) {
				synchronized (OUTPUT_ERROR) {
					e.printStackTrace(OUTPUT_ERROR);
					OUTPUT_ERROR.flush();
				}
			}
		});
	}
}
